Skip to content

feat: dark mode support for pyRevit WPF forms - #3582

Open
romangolev wants to merge 18 commits into
developfrom
feat/forms-dark-mode
Open

feat: dark mode support for pyRevit WPF forms#3582
romangolev wants to merge 18 commits into
developfrom
feat/forms-dark-mode

Conversation

@romangolev

Copy link
Copy Markdown
Member

Summary

  • Central Theme.xaml resource dictionary injects a semantic light/dark palette (resolved from Revit's active UITheme) into every WPFWindow/WPFPanel, and re-templates the standard WPF controls (Button, TextBox, ComboBox, ListBox/ListView, CheckBox, RadioButton, Expander, GroupBox, TabItem, ScrollBar, ContextMenu, MenuItem, ToolTip, Separator, Label) against it, so both first-party dialogs (Settings, Extensions, list pickers, etc.) and third-party extension forms/dockable panels pick up the theme automatically.
  • Native window title bars are re-skinned to match via a new pyRevitLabs.Common.DwmApi wrapper (DWMWA_USE_IMMERSIVE_DARK_MODE on any supported Windows version, exact caption/text colors on Windows 11 22000+), rather than replacing OS chrome with custom-drawn chrome — keeps drag/resize/snap/min/max/close fully native.
  • Live refresh: windows and dockable panes subscribe to Revit's ThemeChanged event and repaint in place when the user toggles Revit's theme, no reload required.
  • Existing XAML across forms/, pyRevitCore.extension, and pyRevitDevTools.extension sample panels was swept for hardcoded light-only colors (White/Black/DimGray/etc.) and switched to the new theme brushes.

Test plan

  • Toggle Revit's UI theme (Light/Dark) and confirm Settings, Extensions, and list-picker dialogs match
  • Confirm a dockable panel (e.g. pyRevitDevTools sample panels) recolors live when the theme is toggled while docked/visible
  • Confirm title bar drag/resize/snap/minimize/maximize/close still work natively
  • Spot-check Revit 2023 or earlier (pre-UITheme) still renders the light palette with no errors

Add a centralized Theme.xaml resource dictionary and inject a semantic
light/dark palette into every WPF control's resources at construction,
selected from Revit's active UITheme. Theme.xaml re-templates the
standard WPF controls (Button, TextBox, ComboBox, ListBox/ListView,
CheckBox, RadioButton, Expander, GroupBox, TabItem, ScrollBar,
ContextMenu, MenuItem, ToolTip, Separator, Label) against
pyRevit<*>Brush resources so descendants pick the active palette via
DynamicResource.

WPFWindow now also recolors the native OS title bar to match: a new
pyRevitLabs.Common.DwmApi wrapper toggles DWMWA_USE_IMMERSIVE_DARK_MODE
on every supported Windows version and additionally sets the exact
caption/text colors on Windows 11 22000+. Both work runs are
no-ops on older Windows. Title bar colors track live theme changes via
a new ThemeChanged subscription; existing form XAMLs were switched
from hardcoded White/Black/DimGray/etc. to those DynamicResource
brushes, and the duplicated DatePicker ControlTemplate in
GetValueWindow.xaml was dropped in favor of the new themed style.
WPFWindow defaults _live_refresh_root_colors to False so a live
ThemeChanged refresh does not clobber the literal transparent
Background that HUD-style overlays (CommandSwitchWindow, SearchPrompt,
...) declare in their own XAML. WPFPanel overrides it to True:
dockable panes stay open for the whole Revit session and never use the
transparent-HUD pattern, so they need the live refresh to actually
repaint their Background/Foreground.

apply_theme was a thin wrapper that just delegated to
setup_resources(set_root_colors=False); collapse it back into
_on_theme_refresh so the per-subclass _live_refresh_root_colors
attribute is the single source of truth.
@devloai

devloai Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Unable to trigger custom agent "Code Reviewer". You have run out of credits 😔
Please upgrade your plan or buy additional credits from the subscription page.

@romangolev romangolev changed the title Dark mode support for pyRevit WPF forms feat: dark mode support for pyRevit WPF forms Aug 21, 2026
@jmcouffin
jmcouffin requested a balanced review from Copilot August 21, 2026 17:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds automatic light/dark theming to all pyRevit WPF forms. A new central Theme.xaml resource dictionary is merged into every WPFWindow/WPFPanel, and a semantic palette (resolved from Revit's active UITheme) is injected as pyRevit*Color/pyRevit*Brush resources by _WPFMixin.setup_resources. Built-in WPF controls are re-templated against those brushes, native title bars are re-skinned via a new DwmApi P/Invoke wrapper, and windows/panes subscribe to Revit's ThemeChanged event to repaint live. Existing first-party XAML is swept to replace hardcoded light-only colors with the new theme brushes.

Changes:

  • New Theme.xaml restyles standard WPF controls; _ipy.py resolves the palette, injects brushes, applies DWM title-bar colors, and wires live ThemeChanged refresh.
  • New pyRevitLabs.Common.DwmApi wrapper for immersive dark mode + caption/text colors.
  • First-party XAML (SelectFromList, GetValueWindow, Settings, Extensions, DevTools panels) swept from hardcoded colors to theme brushes.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
pyrevitlib/pyrevit/forms/Theme.xaml New resource dictionary re-templating standard controls against the semantic palette
pyrevitlib/pyrevit/forms/_ipy.py Palette definition, resource injection, DWM title-bar theming, live ThemeChanged subscribe/refresh
pyrevitlib/pyrevit/forms/SelectFromList.xaml Removed light-only styles; icons bound to theme foreground/brushes
pyrevitlib/pyrevit/forms/GetValueWindow.xaml Dropped hardcoded DatePicker template in favor of theme brushes
dev/pyRevitLabs/pyRevitLabs.Common/Unmanaged.cs New DwmApi wrapper (immersive dark mode + caption/text colors)
extensions/.../SettingsWindow.xaml Swept hardcoded colors to theme brushes; styles BasedOn theme defaults
extensions/.../ExtensionsWindow.xaml Swept hardcoded colors; list/checkbox styles BasedOn theme defaults
extensions/pyRevitDevTools.extension/SamplePanel.xaml Removed Background="White"; styles BasedOn theme defaults
extensions/pyRevitDevTools.extension/DockableExample.xaml Removed hardcoded Background="White"

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread pyrevitlib/pyrevit/forms/Theme.xaml Outdated
Comment thread pyrevitlib/pyrevit/forms/Theme.xaml
Add the editable text box template part to ComboBox so the editor surface picks up the theme brushes, and replace the custom ListViewItem template with style triggers that bind directly to pyRevit theme brushes.
@jmcouffin

Copy link
Copy Markdown
Contributor

ready for review @romangolev ?

Add dark-mode styles for DatePicker, DatePickerTextBox, and Calendar with matching day/month/year chrome, swap the Expander header arrow for a chevron-in-circle, and adjust the dark palette tones for better contrast on hover, pressed, scrollbar, and button surfaces. Also drop the separator rows between settings expanders and tighten their margins.
The custom AnimatedSwitch style hardcoded its own colors so it never picked up dark mode. Replace it with the themed CheckBox across the settings window so toggles follow the active palette. While here, add disabled-state triggers to TextBox, ComboBox, and the Expander toggle, give the expander chevron its own rotation host, and introduce a proper SelectionBackground color in the palette so selected controls render correctly in light and dark.
Resolve the settings window icon via revit.ui.resolve_icon_file so it picks up pyrevit_settings.dark.png under the dark theme, mirroring the bundle icon convention. Hook the resolution into _on_theme_refresh so live theme switches repaint the icon, and ship the new dark asset under netcore, netfx, and the root bin-assets folder.
Promote the inline comments around SetImmersiveDarkMode and SetTitleBarColors into proper XML doc comments, drop the redundant XAML section banners since the style names are self-describing, and reshape the _WPFMixin class docstring plus setup_resources and _live_refresh_root_colors into Google-style Attributes/Important/Note sections.
@romangolev
romangolev marked this pull request as ready for review August 27, 2026 13:18
@romangolev

Copy link
Copy Markdown
Member Author

ready for review @romangolev ?

it's now good enough for evaluation

@romangolev romangolev self-assigned this Aug 27, 2026
@jmcouffin

Copy link
Copy Markdown
Contributor

Quick check

@romangolev

  • font color management in Modelless example
image
  • Settings example
image text in
  • box alignement and padding in color splasher
image
  • Glass effect in extensions
image
  • Not sure this is doable: Dialogs
image

@jmcouffin

Copy link
Copy Markdown
Contributor

btw, this is cool...
theme.xaml review is way too much...

@jmcouffin

Copy link
Copy Markdown
Contributor

the unselected stat of the check box lack some contrast a bit.
image

jmcouffin and others added 6 commits August 27, 2026 18:02
Hardcoded accent backgrounds in user XAML — danger and confirm buttons, branded swatches — stayed legible in light mode but vanished under dark mode because the inherited near-white foreground never contrasted against them. Walk the logical tree once on window construction and on every theme refresh; for every element that authored a background (skipping inherited/default values and theme brushes), pick whichever of pyRevitWindowForegroundBrush or pyRevitWindowBackgroundBrush scores higher per WCAG contrast and assign it to the matching foreground dependency property. Add ControlOverlayHover/Pressed, DangerBackground, and SuccessBackground to both palettes, and refactor Button/ListBoxItem templates to paint hover and pressed through a transparent overlay Border so the contrast pass owns the actual text color. Theme a Slider from scratch with a round thumb and themed track, restore ListViewItem template so non-GridView Views still render, swap icon brushes in SettingsWindow/ExtensionsWindow/SelectFromList to DynamicResource, and replace the example/test pushbutton hex colors with the new danger/success brushes. SettingsWindow now generates per-control attributes rather than implicit Window.Resources styles — implicit styles shadow the merged Theme.xaml styles — wraps the schema in a ScrollViewer with a max_height cap, and ColorSplasher drops its custom button overrides, normalizes control heights, and disables virtualization so _update_listbox_colors can reach every ListBoxItem.
The previous styling pass bumped ColorSplasher ComboBox, Button, and TextBox heights to 24 and the themed Button/ComboBox default Padding to 8,3 / 6,3, which made rows noticeably taller than the surrounding labels and the legend stack. Revert ColorSplasher controls back to 20 and tighten the themed defaults to 8,1 / 6,1 so they sit flush with neighbouring controls.
Theme.xaml: add a horizontal-bar Path alongside the check mark and an IsChecked={x:Null} trigger so an indeterminate CheckBox renders a dash, recolor that path in the disabled-state trigger, and let the caption recognize access keys. _ipy.py: tighten the contrast pass to skip translucent backgrounds (alpha != 0xFF) — their effective color depends on what is underneath — and drop the now-unused _CONTRAST_MIN_ALPHA constant; expand the apply_contrast_foregrounds docstring to record the new rule. settings_window.py: remove the now-redundant inline comments above the layout constants since the names already carry the intent.
Theme.xaml: the themed CheckBox template put the check and indeterminate Paths as siblings directly inside a 14x14 Border. Decorator only accepts one child, so the indeterminate bar never rendered. Wrap both in a Grid so they share the box and the IsChecked={x:Null} trigger can show the bar. Also note next to the ListViewItem template why an explicit one is required — the default Aero template paints its own glossy gradient and ignores Background/Foreground. hooks.py: guard get_event_hooks against a missing handler and return [] instead of crashing on None.GetAllEventHooks().
@romangolev
romangolev requested a review from jmcouffin August 28, 2026 15:33
@romangolev

Copy link
Copy Markdown
Member Author

made adjustments, thanks for comments!

  • Not sure this is doable: Dialogs

Keeping them like this for now. Even Autodesk haven't decide what to do with them for now. They are going to have a parity with same Revit OG forms =)

Everything else seems to be fixed, please double check it before merging.

theme.xaml review is way too much...

What do you mean by that? Just a big file or is it hard to read?

Title bars and the settings window action bar are chrome surfaces that should be visually distinct from regular controls. Introduce a dedicated ChromeBackground palette key and route both surfaces to it instead of sharing ControlBackground.
AutoDimWindow painted its own light surfaces -- a #FFF7F7F7 page and five
White cards -- while its controls and inherited text color came from the
active pyRevit palette. Under a dark theme that put near-white text and
dark input fields on white cards.

Removing the attributes lets the Window style in Theme.xaml supply the
background and lets TextElement.Foreground inherit from the window root,
so the dialog tracks either theme with nothing left to keep in sync. The
cards lose their raised surface and read as spacing-separated sections.
@romangolev

Copy link
Copy Markdown
Member Author

Double-checked, got a little more fixes

Screenshot 2026-08-29 060820 Screenshot 2026-08-29 061900 Screenshot 2026-08-29 061824 Screenshot 2026-08-29 062050 Screenshot 2026-08-29 061842 Screenshot 2026-08-29 062103 Screenshot 2026-08-29 061729 Screenshot 2026-08-29 062117

@jmcouffin

Copy link
Copy Markdown
Contributor

What do you mean by that? Just a big file or is it hard to read?
That it requires a lot of headspace. #ramExhaustion

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 17 changed files in this pull request and generated 1 comment.

Comment thread pyrevitlib/pyrevit/forms/GetValueWindow.xaml Outdated
CustomDatePickerStyle declared its own setters but omitted BasedOn, so it replaced the base DatePicker style wholesale and dropped the theme-aware setters the base contributes (including the themed palette added earlier in this branch). Reference the base type so the custom setters layer on top instead of starting from scratch.
@romangolev

Copy link
Copy Markdown
Member Author

That it requires a lot of headspace. #ramExhaustion

RAM-wise, the load that PR produce us slim. Headspace-wise just a big bulky Theme file.

Btw, got the issue founded by copilot covered. Should be good to go ^

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants